﻿using System.Collections.Generic;

namespace Raven
{
    public class RavenWeaponSystem
    {
        private Dictionary<WeaponType, RavenWeapon> _weaponMap;

        private RavenBot _owner;

        private RavenWeapon _currentWeapon;

        //this is the minimum amount of time a bot needs to see an opponent before
        //it can react to it. This variable is used to prevent a bot shooting at
        //an opponent the instant it becomes visible.
        private Fix _reactionTime;

        //each time the current weapon is fired a certain amount of random noise is
        //added to the the angle of the shot. This prevents the bots from hitting
        //their opponents 100% of the time. The lower this value the more accurate
        //a bot's aim will be. Recommended values are between 0 and 0.2 (the value
        //represents the max deviation in radians that can be added to each shot).
        private Fix _aimAccuracy;

        //the amount of time a bot will continue aiming at the position of the target
        //even if the target disappears from view.
        private Fix _aimPersistance;

        //predicts where the target will be by the time it takes the current weapon's
        //projectile type to reach it. Used by TakeAimAndShoot
        Vector2 PredictFuturePositionOfTarget()
        {
            Fix MaxSpeed = GetCurrentWeapon().GetMaxProjectileSpeed();

            //if the target is ahead and facing the agent shoot at its current pos
            Vector2 ToEnemy = _owner.GetTargetBot().Pos() - _owner.Pos();

            //the lookahead time is proportional to the distance between the enemy
            //and the pursuer; and is inversely proportional to the sum of the
            //agent's velocities
            Fix LookAheadTime = ToEnemy.magnitude /
                                  (MaxSpeed + _owner.GetTargetBot().MaxSpeed());

            //return the predicted future position of the enemy
            return _owner.GetTargetBot().Pos() +
                   _owner.GetTargetBot().Velocity() * LookAheadTime;
        }

        //adds a random deviation to the firing angle not greater than m_dAimAccuracy 
        //rads
        void AddNoiseToAim(ref Vector2 AimingPos)
        {
            Vector2 toPos = AimingPos - _owner.Pos();

            Transformations.Vec2DRotateAroundOrigin(ref toPos, _owner.GetWorld().GetRandom().RandInRange(-_aimAccuracy, _aimAccuracy));

            AimingPos = toPos + _owner.Pos();
        }

        public RavenWeaponSystem(RavenBot owner,
            Fix ReactionTime,
            Fix AimAccuracy,
            Fix AimPersistance)
        {
            _owner = owner;
            _reactionTime = ReactionTime;
            _aimAccuracy = AimAccuracy;
            _aimPersistance = AimPersistance;
        }

        public void Initialize()
        {
            //delete any existing weapons
            if (_weaponMap != null)
            {
                foreach (var cur in _weaponMap)
                {
                    cur.Value.Destroy();
                }
            }

            _weaponMap = new Dictionary<WeaponType, RavenWeapon>();

            //set up the container
            _weaponMap.Add(WeaponType.type_blaster, new Blaster(_owner));
            // _weaponMap.Add(WeaponType.type_rail_gun, new RailGun(_owner));
            // _weaponMap.Add(WeaponType.type_rocket_launcher, new RocketLauncher(_owner));
            // _weaponMap.Add(WeaponType.type_shotgun, new ShotGun(_owner));
            _currentWeapon = _weaponMap[WeaponType.type_blaster];
        }

        //this method aims the bot's current weapon at the target (if there is a
        //target) and, if aimed correctly, fires a round. (Called each update-step
        //from Raven_Bot::Update)
        public void TakeAimAndShoot()
        {
            //aim the weapon only if the current target is shootable or if it has only
            //very recently gone out of view (this latter condition is to ensure the 
            //weapon is aimed at the target even if it temporarily dodges behind a wall
            //or other cover)
            if (_owner.GetTargetSys().isTargetShootable() ||
                (_owner.GetTargetSys().GetTimeTargetHasBeenOutOfView() <
                 _aimPersistance))
            {
                //the position the weapon will be aimed at
                Vector2 AimingPos = _owner.GetTargetBot().Pos();

                //if the current weapon is not an instant hit type gun the target position
                //must be adjusted to take into account the predicted movement of the 
                //target
                if (GetCurrentWeapon().GetWeaponType() == WeaponType.type_rocket_launcher ||
                    GetCurrentWeapon().GetWeaponType() == WeaponType.type_blaster)
                {
                    AimingPos = PredictFuturePositionOfTarget();

                    //if the weapon is aimed correctly, there is line of sight between the
                    //bot and the aiming position and it has been in view for a period longer
                    //than the bot's reaction time, shoot the weapon
                    if (_owner.RotateFacingTowardPosition(AimingPos) &&
                        (_owner.GetTargetSys().GetTimeTargetHasBeenVisible() >
                         _reactionTime) &&
                        _owner.hasLOSto(AimingPos))
                    {
                        AddNoiseToAim(ref AimingPos);

                        GetCurrentWeapon().ShootAt(AimingPos);
                    }
                }

                //no need to predict movement, aim directly at target
                else
                {
                    //if the weapon is aimed correctly and it has been in view for a period
                    //longer than the bot's reaction time, shoot the weapon
                    if (_owner.RotateFacingTowardPosition(AimingPos) &&
                        (_owner.GetTargetSys().GetTimeTargetHasBeenVisible() >
                         _reactionTime))
                    {
                        AddNoiseToAim(ref AimingPos);

                        GetCurrentWeapon().ShootAt(AimingPos);
                    }
                }
            }

            //no target to shoot at so rotate facing to be parallel with the bot's
            //heading direction
            else
            {
                _owner.RotateFacingTowardPosition(_owner.Pos() + _owner.Heading());
            }
        }

        //this method determines the most appropriate weapon to use given the current
        //game state. (Called every n update-steps from Raven_Bot::Update)
        public void SelectWeapon()
        {
            //if a target is present use fuzzy logic to determine the most desirable 
            //weapon.
            if (_owner.GetTargetSys().isTargetPresent())
            {
                //calculate the distance to the target
                Fix DistToTarget = (_owner.Pos() - _owner.GetTargetSys().GetTarget().Pos()).magnitude;

                //for each weapon in the inventory calculate its desirability given the 
                //current situation. The most desirable weapon is selected
                Fix BestSoFar = Fix.MinValue;

                foreach (var curWeap in _weaponMap)
                {
                    //grab the desirability of this weapon (desirability is based upon
                    //distance to target and ammo remaining)
                    Fix score = curWeap.Value.GetDesirability(DistToTarget);

                    //if it is the most desirable so far select it
                    if (score > BestSoFar)
                    {
                        BestSoFar = score;

                        //place the weapon in the bot's hand.
                        _currentWeapon = curWeap.Value;
                    }
                }
            }

            // else
            // {
            //     _currentWeapon = _weaponMap[WeaponType.type_blaster];
            // }
        }

        //this will add a weapon of the specified type to the bot's inventory. 
        //If the bot already has a weapon of this type only the ammo is added. 
        //(called by the weapon giver-triggers to give a bot a weapon)
        public void AddWeapon(WeaponType weapon_type)
        {
            //create an instance of this weapon
            RavenWeapon w = null;

            switch (weapon_type)
            {
                case WeaponType.type_rail_gun:

                    w = new RailGun(_owner);
                    break;

                case WeaponType.type_shotgun:

                    w = new ShotGun(_owner);
                    break;

                case WeaponType.type_rocket_launcher:

                    w = new RocketLauncher(_owner);
                    break;
                default:
                    Logger.LogError(weapon_type.ToString() + " can't add");
                    break;
            } //end switch


            //if the bot already holds a weapon of this type, just add its ammo
            RavenWeapon present = GetWeaponFromInventory(weapon_type);

            if (present != null)
            {
                present.IncrementRounds(w.NumRoundsRemaining());
            }

            //if not already holding, add to inventory
            else
            {
                _weaponMap[weapon_type] = w;
            }
        }

        //changes the current weapon to one of the specified type (provided that type
        //is in the bot's possession)
        public void ChangeWeapon(WeaponType type)
        {
            RavenWeapon w = GetWeaponFromInventory(type);

            if (w != null) _currentWeapon = w;
        }

        //shoots the current weapon at the given position
        public void ShootAt(Vector2 pos)
        {
            GetCurrentWeapon().ShootAt(pos);
        }

        //returns a pointer to the current weapon
        public RavenWeapon GetCurrentWeapon()
        {
            return _currentWeapon;
        }

        //returns a pointer to the specified weapon type (if in inventory, null if 
        //not)
        public RavenWeapon GetWeaponFromInventory(WeaponType weapon_type)
        {
            if (!_weaponMap.ContainsKey(weapon_type))
            {
                return null;
            }

            return _weaponMap[weapon_type];
        }

        //returns the amount of ammo remaining for the specified weapon
        public int GetAmmoRemainingForWeapon(WeaponType weapon_type)
        {
            if (_weaponMap.ContainsKey(weapon_type))
            {
                return _weaponMap[weapon_type].NumRoundsRemaining();
            }

            return 0;
        }

        public Fix ReactionTime()
        {
            return _reactionTime;
        }
    }
}